/*   
 * Copyright 2010 Rob Schoening
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 *    
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

/*
 * Substantial portions of this source file have been copied from 
 * Vaadin's BeanItemContainer
 */

package org.beaconframework.vaadin.data.provider;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.Container.Filterable;
import com.vaadin.data.Container.Indexed;
import com.vaadin.data.Container.ItemSetChangeNotifier;
import com.vaadin.data.Container.Sortable;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.Property.ValueChangeNotifier;
import com.vaadin.data.util.BeanItem;
import com.vaadin.data.util.DefaultItemSorter;
import com.vaadin.data.util.Filter;
import com.vaadin.data.util.ItemSorter;

/**
 * An {@link ArrayList} backed container for {@link BeanItem}s.
 * <p>
 * Bean objects act as identifiers. For this reason, they should implement
 * Object.equals(Object) and Object.hashCode().
 * </p>
 * 
 * @param <BT>
 * 
 * @since 5.4
 */
@SuppressWarnings("serial")
public abstract class AbstractItemContainer<BT> implements Indexed, Sortable,
        Filterable, ItemSetChangeNotifier, ValueChangeListener {

    private static Logger log = LoggerFactory
            .getLogger(AbstractItemContainer.class);
    // filtered and unfiltered item IDs
    private ArrayList<BT> list = new ArrayList<BT>();
    private ArrayList<BT> allItems = new ArrayList<BT>();
    private final Map<BT, Item> beanToItem = new HashMap<BT, Item>();
    private Map<Object, PropertyDefinition> propertyDefMap = new ConcurrentHashMap<Object, PropertyDefinition>();

    private List<ItemSetChangeListener> itemSetChangeListeners;

    private Set<Filter> filters = new HashSet<Filter>();
    public static final EmptyProperty EMPTY_PROPERTY = new EmptyProperty();
    /**
     * The item sorter which is used for sorting the container.
     */
    private ItemSorter itemSorter = new DefaultItemSorter();

    public static final class EmptyProperty implements Property {

        private EmptyProperty() {
            super();
            // TODO Auto-generated constructor stub
        }


        @Override
        public Class<?> getType() {

            return null;
        }


        @Override
        public Object getValue() {

            return null;
        }


        @Override
        public boolean isReadOnly() {

            return false;
        }


        @Override
        public void setReadOnly(boolean newStatus) {

        }


        @Override
        public void setValue(Object newValue) throws ReadOnlyException,
                ConversionException {
            // TODO Auto-generated method stub

        }

    }

    public final class PropertyDefinition {
        private Object propertyId;
        private Object defaultValue;
        private Class<?> propertyType;
        private Object containerConfig;


        public Object getPropertyId() {
            return propertyId;
        }


        public void setPropertyId(Object propertyId) {
            this.propertyId = propertyId;
        }


        public Object getDefaultValue() {
            return defaultValue;
        }


        public void setDefaultValue(Object defaultValue) {
            this.defaultValue = defaultValue;
        }


        public Class<?> getPropertyType() {
            return propertyType;
        }


        public void setPropertyType(Class<?> propertyType) {
            this.propertyType = propertyType;
        }


        public Object getContainerConfig() {
            return containerConfig;
        }


        public void setContainerConfig(Object containerConfig) {
            this.containerConfig = containerConfig;
        }

    }


    /**
     * Constructs BeanItemContainer for beans of a given type.
     * 
     * @param type
     *            the class of beans to be used with this containers.
     * @throws IllegalArgumentException
     *             If the type is null
     */
    public AbstractItemContainer() {
        super();
    }


    /**
     * Unsupported operation.
     * 
     * @see com.vaadin.data.Container.Indexed#addItemAt(int)
     */
    public Object addItemAt(int index) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }


    /**
     * Adds new item at given index.
     * 
     * The bean is used both as the item contents and as the item identifier.
     * 
     * @see com.vaadin.data.Container.Indexed#addItemAt(int, Object)
     */
    public Item addItemAt(int index, Object newItemId)
            throws UnsupportedOperationException {
        if (index < 0 || index > size()) {
            return null;
        } else if (index == 0) {
            // add before any item, visible or not
            return addItemAtInternalIndex(0, newItemId);
        } else {
            // if index==size(), adds immediately after last visible item
            return addItemAfter(getIdByIndex(index - 1), newItemId);
        }
    }


    /**
     * Adds new item at given index of the internal (unfiltered) list.
     * <p>
     * The item is also added in the visible part of the list if it passes the
     * filters.
     * </p>
     * 
     * @param index
     *            Internal index to add the new item.
     * @param newItemId
     *            Id of the new item to be added.
     * @return Returns new item or null if the operation fails.
     */
    @SuppressWarnings("unchecked")
    private Item addItemAtInternalIndex(int index, Object newItemId) {
        // Make sure that the Item has not been created yet
        if (allItems.contains(newItemId)) {
            return null;
        }
        if (supports(newItemId.getClass())) {
            BT pojo = (BT) newItemId;
            // "list" will be updated in filterAll()
            allItems.add(index, pojo);
            Item beanItem = newItem(pojo);
            beanToItem.put(pojo, beanItem);
            // add listeners to be able to update filtering on property changes
            for (Filter filter : filters) {
                // addValueChangeListener avoids adding duplicates
                Object propertyId = null;
                try {
                    propertyId = workaroundGetPropertyId(filter);
                } catch (Exception e) {
                    log.warn("visibility workaround failed", e);
                }
                if (propertyId != null) {
                    addValueChangeListener(beanItem, propertyId); // TODO
                }

            }

            // it is somewhat suboptimal to filter all items
            filterAll();
            return beanItem;
        } else {
            return null;
        }
    }


    public static Object workaroundGetPropertyId(Filter filter)
            throws SecurityException, NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {
        // TODO Auto-generated method stub

        java.lang.reflect.Field f = Filter.class.getDeclaredField("propertyId");
        if (!f.isAccessible()) {
            log.debug("Making Filter.propertyId visible for workaround");
            f.setAccessible(true);
        }
        return f.get(filter);

    }


    public BT getIdByIndex(int index) {
        return list.get(index);
    }


    public int indexOfId(Object itemId) {
        return list.indexOf(itemId);
    }


    /**
     * Unsupported operation.
     * 
     * @see com.vaadin.data.Container.Ordered#addItemAfter(Object)
     */
    public Object addItemAfter(Object previousItemId)
            throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }


    /**
     * Adds new item after the given item.
     * 
     * The bean is used both as the item contents and as the item identifier.
     * 
     * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object)
     */
    public Item addItemAfter(Object previousItemId, Object newItemId)
            throws UnsupportedOperationException {
        // only add if the previous item is visible
        if (containsId(previousItemId)) {
            return addItemAtInternalIndex(allItems.indexOf(previousItemId) + 1,
                    newItemId);
        } else {
            return null;
        }
    }


    public BT firstItemId() {
        if (size() > 0) {
            return getIdByIndex(0);
        } else {
            return null;
        }
    }


    public boolean isFirstId(Object itemId) {
        return firstItemId() == itemId;
    }


    public boolean isLastId(Object itemId) {
        return lastItemId() == itemId;
    }


    public BT lastItemId() {
        if (size() > 0) {
            return getIdByIndex(size() - 1);
        } else {
            return null;
        }
    }


    public BT nextItemId(Object itemId) {
        int index = indexOfId(itemId);
        if (index >= 0 && index < size() - 1) {
            return getIdByIndex(index + 1);
        } else {
            // out of bounds
            return null;
        }
    }


    public BT prevItemId(Object itemId) {
        int index = indexOfId(itemId);
        if (index > 0) {
            return getIdByIndex(index - 1);
        } else {
            // out of bounds
            return null;
        }
    }


    public boolean addContainerProperty(Object propertyId, Class<?> type,
            Object defaultValue) throws UnsupportedOperationException {
        PropertyDefinition pd = new PropertyDefinition();
        pd.setPropertyType(type);
        pd.setDefaultValue(defaultValue);
        pd.setPropertyId(propertyId);
        propertyDefMap.put(propertyId, pd);
        return true;
    }


    /**
     * Unsupported operation.
     * 
     * @see com.vaadin.data.Container#addItem()
     */
    public Object addItem() throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }


    /**
     * Creates a new Item with the bean into the Container.
     * 
     * The bean is used both as the item contents and as the item identifier.
     * 
     * @see com.vaadin.data.Container#addItem(Object)
     */
    public Item addObject(BT bean) {
        return addItem(bean);
    }


    /**
     * Creates a new Item with the bean into the Container.
     * 
     * The bean is used both as the item contents and as the item identifier.
     * 
     * @see com.vaadin.data.Container#addItem(Object)
     */

    public Item addItem(Object itemId) throws UnsupportedOperationException {
        if (size() > 0) {
            // add immediately after last visible item
            int lastIndex = allItems.indexOf(lastItemId());
            return addItemAtInternalIndex(lastIndex + 1, itemId);
        } else {
            return addItemAtInternalIndex(0, itemId);
        }
    }


    @Override
    public boolean containsId(Object itemId) {
        // only look at visible items after filtering
        return list.contains(itemId);
    }


    @Override
    public final Property getContainerProperty(Object itemId, Object propertyId) {
        Item item = getItem(itemId);
        if (item == null) {
            return null;
        }
        Property p = getItem(itemId).getItemProperty(propertyId);

        return p;
    }


    @Override
    public Item getItem(Object itemId) {
        return beanToItem.get(itemId);
    }


    @SuppressWarnings("unchecked")
    @Override
    public Collection<BT> getItemIds() {
        return (Collection<BT>) list.clone();
    }


    @Override
    public boolean removeAllItems() throws UnsupportedOperationException {
        allItems.clear();
        list.clear();
        // detach listeners from all BeanItems
        for (Item item : beanToItem.values()) {
            removeAllValueChangeListeners(item);
        }
        beanToItem.clear();
        fireItemSetChange();
        return true;
    }


    public boolean removeContainerProperty(Object propertyId)
            throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }


    public boolean removeItem(Object itemId)
            throws UnsupportedOperationException {
        if (!allItems.remove(itemId)) {
            return false;
        }
        // detach listeners from Item
        removeAllValueChangeListeners(getItem(itemId));
        // remove item
        beanToItem.remove(itemId);
        list.remove(itemId);
        fireItemSetChange();
        return true;
    }


    private void addValueChangeListener(Item beanItem, Object propertyId) {
        Property property = beanItem.getItemProperty(propertyId);
        if (property instanceof ValueChangeNotifier) {
            // avoid multiple notifications for the same property if
            // multiple filters are in use
            ValueChangeNotifier notifier = (ValueChangeNotifier) property;
            notifier.removeListener(this);
            notifier.addListener(this);
        }
    }


    private void removeValueChangeListener(Item item, Object propertyId) {
        Property property = item.getItemProperty(propertyId);
        if (property instanceof ValueChangeNotifier) {
            ((ValueChangeNotifier) property).removeListener(this);
        }
    }


    private void removeAllValueChangeListeners(Item item) {
        for (Object propertyId : item.getItemPropertyIds()) {
            removeValueChangeListener(item, propertyId);
        }
    }


    public int size() {
        return list.size();
    }


    public Collection<Object> getSortableContainerPropertyIds() {
        LinkedList<Object> sortables = new LinkedList<Object>();
        for (Object propertyId : getContainerPropertyIds()) {
            Class<?> propertyType = getType(propertyId);
            if (Comparable.class.isAssignableFrom(propertyType)) {
                sortables.add(propertyId);
            }
        }
        return sortables;
    }


    /*
     * (non-Javadoc)
     * 
     * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
     * boolean[])
     */
    public void sort(Object[] propertyId, boolean[] ascending) {
        itemSorter.setSortProperties(this, propertyId, ascending);

        doSort();

        // notifies if anything changes in the filtered list, including order
        filterAll();
    }


    /**
     * Perform the sorting of the data structures in the container. This is
     * invoked when the <code>itemSorter</code> has been prepared for the sort
     * operation. Typically this method calls
     * <code>Collections.sort(aCollection, getItemSorter())</code> on all arrays
     * (containing item ids) that need to be sorted.
     * 
     */
    protected void doSort() {
        Collections.sort(allItems, getItemSorter());
    }


    public void addListener(ItemSetChangeListener listener) {
        if (itemSetChangeListeners == null) {
            itemSetChangeListeners = new LinkedList<ItemSetChangeListener>();
        }
        itemSetChangeListeners.add(listener);
    }


    public void removeListener(ItemSetChangeListener listener) {
        if (itemSetChangeListeners != null) {
            itemSetChangeListeners.remove(listener);
        }
    }


    private void fireItemSetChange() {
        if (itemSetChangeListeners != null) {
            final Container.ItemSetChangeEvent event = new Container.ItemSetChangeEvent() {
                public Container getContainer() {
                    return AbstractItemContainer.this;
                }
            };
            for (ItemSetChangeListener listener : itemSetChangeListeners) {
                listener.containerItemSetChange(event);
            }
        }
    }


    @SuppressWarnings("unchecked")
    public void addContainerFilter(Object propertyId, String filterString,
            boolean ignoreCase, boolean onlyMatchPrefix) {

        if (filters.isEmpty()) {
            list = (ArrayList<BT>) allItems.clone();
        }
        // listen to change events to be able to update filtering
        for (Item item : beanToItem.values()) {
            addValueChangeListener(item, propertyId);
        }
        try {
            Filter f = AbstractItemContainer.workaroundNewFilter(propertyId,
                    filterString, ignoreCase, onlyMatchPrefix);
            filter(f);
            filters.add(f);

        } catch (Exception e) {
            log.warn("", e);
        }
        fireItemSetChange();

    }


    /**
     * Filter the view to recreate the visible item list from the unfiltered
     * items, and send a notification if the set of visible items changed in any
     * way.
     */
    @SuppressWarnings("unchecked")
    protected void filterAll() {
        // avoid notification if the filtering had no effect
        List<BT> originalItems = list;
        // it is somewhat inefficient to do a (shallow) clone() every time
        list = (ArrayList<BT>) allItems.clone();
        for (Filter f : filters) {
            filter(f);
        }
        // check if exactly the same items are there after filtering to avoid
        // unnecessary notifications
        // this may be slow in some cases as it uses BT.equals()
        if (!originalItems.equals(list)) {
            fireItemSetChange();
        }
    }


    protected void filter(Filter f) {
        Iterator<BT> iterator = list.iterator();
        while (iterator.hasNext()) {
            BT bean = iterator.next();
            if (!f.passesFilter(getItem(bean))) {
                iterator.remove();
            }
        }
    }


    public void removeAllContainerFilters() {
        if (!filters.isEmpty()) {
            filters = new HashSet<Filter>();
            // stop listening to change events for any property
            for (Item item : beanToItem.values()) {
                removeAllValueChangeListeners(item);
            }
            filterAll();
        }
    }


    public void removeContainerFilters(Object propertyId) {

        if (!filters.isEmpty()) {
            for (Iterator<Filter> iterator = filters.iterator(); iterator
                    .hasNext();) {
                Filter f = iterator.next();
                try {
                    Object filterPropertyId = workaroundGetPropertyId(f);
                    if (filterPropertyId.equals(propertyId)) {
                        iterator.remove();
                    }
                } catch (Exception e) {

                    log.error("workaround failed", e);
                }
            }
            // stop listening to change events for the property
            for (Item item : beanToItem.values()) {
                removeValueChangeListener(item, propertyId);
            }
            filterAll();
        }

    }


    public void valueChange(ValueChangeEvent event) {
        // if a property that is used in a filter is changed, refresh filtering
        filterAll();
    }


    public ItemSorter getItemSorter() {
        return itemSorter;
    }


    public void setItemSorter(ItemSorter itemSorter) {
        this.itemSorter = itemSorter;
    }


    protected boolean supports(Class<? extends Object> classToCHeck) {
        return true;
    }


    protected abstract Item newItem(BT pojo);


    protected PropertyDefinition getPropertyDefinition(Object id) {
        return propertyDefMap.get(id);
    }


    @Override
    public Collection<?> getContainerPropertyIds() {
        return Collections.unmodifiableSet(propertyDefMap.keySet());
    }


    @Override
    public Class<?> getType(Object propertyId) {
        PropertyDefinition def = propertyDefMap.get(propertyId);
        if (def == null) {
            return null;
        } else {
            return def.getPropertyType();
        }
    }


    public static Filter workaroundNewFilter(Object propertyId,
            String filterString, boolean ignoreCase, boolean onlyMatchPrefix)
            throws SecurityException, NoSuchMethodException,
            IllegalArgumentException, InstantiationException,
            IllegalAccessException, InvocationTargetException {
        Constructor<Filter> m = (Constructor<Filter>) Filter.class
                .getDeclaredConstructor(Object.class, String.class,
                        boolean.class, boolean.class);

        if (!m.isAccessible()) {
            log.debug("Making constructor accessible: " + m);
            m.setAccessible(true);
        }
        Object result = m.newInstance(propertyId, filterString, ignoreCase,
                onlyMatchPrefix);

        return (Filter) result;
    }

}
